package cc.aliza.lib.mongo.core.utils;

import cc.aliza.lib.mongo.core.AlizaConnection;
import cc.aliza.lib.mongo.core.DBIndex;
import cc.aliza.lib.mongo.core.annotations.*;
import cc.aliza.lib.mongo.core.cache.ConstructorCache;
import cc.aliza.lib.mongo.core.cache.FieldsCache;
import cc.aliza.lib.mongo.core.decoder.Decoder;
import cc.aliza.lib.mongo.core.decoder.DecoderFactory;
import cc.aliza.lib.mongo.core.encoder.Encoder;
import cc.aliza.lib.mongo.core.encoder.EncoderFactory;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.model.IndexOptions;
import org.bson.Document;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 『给时光以生命·给岁月以文明』
 *
 * @author Zhang Jing (byrdkm17@gmail.com).
 */
@SuppressWarnings("unchecked")
public final class MapperUtil {

    public static <T> T fromDocument(Class<T> clazz, Document document) {
        if (document == null) {
            return null;
        }
        T obj = ConstructorCache.getInstance().get(clazz);
        Field[] fields = FieldsCache.getInstance().get(clazz);
        Arrays.stream(fields).forEach(field -> {
            Decoder decoder = DecoderFactory.create(document, field);
            if (decoder != null && !decoder.isNullField()) {
                decoder.decode(obj);
            }
        });
        return obj;
    }

    public static Document toDocument(Object obj) {
        Document document = toDocument(obj, false);
        document.remove("_impl");
        return document;
    }

    public static Document toDocument(Object obj, boolean isReduce) {
        if (obj == null) {
            return null;
        }
        Document document = new Document();
        document.put("_impl", obj.getClass().getSimpleName());
        Field[] fields = FieldsCache.getInstance().get(obj.getClass());
        Arrays.stream(fields).forEach(field -> {
            if (!isReduce || field.getAnnotation(Reduce.class) == null) {
                Encoder encoder = EncoderFactory.create(obj, field);
                if (encoder != null && !encoder.isNullField()) {
                    document.put(encoder.getFieldName(), encoder.encode());
                }
            }
        });
        return document;
    }

    public static String getEntityName(Class<?> clazz) {
        Entity entity = clazz.getAnnotation(Entity.class);
        String name = entity.name();
        if (name.equals(Default.NAME)) {
            name = nameTransform(clazz.getSimpleName());
        }
        return name;
    }

    public static String getDBName(Class<?> clazz) {
        Entity entity = clazz.getAnnotation(Entity.class);
        String database = entity.database();
        String prefix = AlizaConnection.getInstance().getPrefix();
        if (!StringUtil.isEmpty(prefix)) {
            database = prefix + "_" + database;
        }
        return database + "." + getEntityName(clazz);
    }

    private static String nameTransform(String name) {
        return name.replaceAll("[A-Z][a-z]+", "_$0_").replaceAll("__", "_").replaceAll("^_|_$", "").toLowerCase();
    }

    public static Document getKeyFields(Class<?> clazz) {
        Document keys = new Document();
        Field[] fields = FieldsCache.getInstance().get(clazz);
        Arrays.stream(fields).forEach(field -> {
            String fieldName = field.getName();
            Property property = field.getAnnotation(Property.class);
            if (property != null && property.lazy()) {
                keys.put(getName(property.name(), fieldName), 0);
            }
            Embed embed = field.getAnnotation(Embed.class);
            if (embed != null && embed.lazy()) {
                keys.put(getName(embed.name(), fieldName), 0);
            }
            EmbedList embedList = field.getAnnotation(EmbedList.class);
            if (embedList != null && embedList.lazy()) {
                keys.put(getName(embedList.name(), fieldName), 0);
            }
        });
        return keys;
    }

    private static String getName(String name, String defaultName) {
        if (!Default.NAME.equals(name)) {
            defaultName = name;
        }
        return defaultName;
    }

    public static Document getSort(String orderBy) {
        Document sort = new Document();
        orderBy = orderBy.replaceAll(" +", "");
        orderBy = orderBy.replaceAll("[{}']", "");
        String[] arr = orderBy.split(",");
        Arrays.stream(arr).forEach(o -> {
            String[] kv = o.split(":");
            if (kv[0].equals("id")) {
                kv[0] = Operator.ID;
            }
            sort.put(kv[0], Integer.parseInt(kv[1]));
        });
        return sort;
    }

    public static List<DBIndex> getDBIndex(String[] index) {
        List<DBIndex> list = new ArrayList<>();

        Arrays.stream(index).forEach(o -> {
            o = o.replaceAll(" +", "");
            String[] group = o.split("\\},\\{");

            Document keys = new Document();

            String keyStr = group[0].replaceAll("[{}']", "");
            Arrays.stream(keyStr.split(",")).forEach(keyValue -> {
                String[] kv = keyValue.split(":");
                keys.put(kv[0], toBoolean(kv[1]));
            });

            IndexOptions options = new IndexOptions();
            if (group.length > 1) {
                options.background(true);

                String optionStr = group[1].replaceAll("[{}']", "");
                Arrays.stream(optionStr.split(",")).forEach(keyValue -> {
                    String[] kv = keyValue.split(":");

                    if ("unique".equals(kv[0])) {
                        options.unique(toBoolean(kv[1]));
                        return;
                    }

                    if ("name".equals(kv[0])) {
                        options.name(kv[1]);
                        return;
                    }

                    if ("spare".equals(kv[0])) {
                        options.sparse(toBoolean(kv[1]));
                        return;
                    }

                    if ("expireAfterSeconds".equals(kv[0])) {
                        options.expireAfter(Long.valueOf(kv[1]), TimeUnit.SECONDS);
                        return;
                    }

                    if ("version".equals(kv[0])) {
                        options.version(Integer.valueOf(kv[1]));
                        return;
                    }

                    if ("weights".equals(kv[0])) {
                        return;
                    }

                    if ("defaultLanguage".equals(kv[0])) {
                        options.defaultLanguage(kv[1]);
                        return;
                    }

                    if ("languageOverride".equals(kv[0])) {
                        options.languageOverride(kv[1]);
                        return;
                    }

                    if ("textIndexVersion".equals(kv[0])) {
                        options.textVersion(Integer.valueOf(kv[1]));
                        return;
                    }

                    if ("sphereIndexVersion".equals(kv[0])) {
                        options.sphereVersion(Integer.valueOf(kv[1]));
                        return;
                    }

                    if ("bits".equals(kv[0])) {
                        options.bits(Integer.valueOf(kv[1]));
                        return;
                    }

                    if ("min".equals(kv[0])) {
                        options.min(Double.valueOf(kv[1]));
                        return;
                    }

                    if ("max".equals(kv[0])) {
                        options.max(Double.valueOf(kv[1]));
                        return;
                    }

                    if ("bucketSize".equals(kv[0])) {
                        options.bucketSize(Double.valueOf(kv[1]));
                    }
                });
            }
            list.add(new DBIndex(keys, options));
        });
        return list;
    }

    private static boolean toBoolean(String src) {
        return !("-1".equals(src) || "false".equals(src));
    }

    public static <T> List<T> toList(Class<T> clazz, MongoCursor<Document> cursor) {
        List<T> list = new ArrayList<>();
        while (cursor.hasNext()) {
            list.add(fromDocument(clazz, cursor.next()));
        }
        cursor.close();
        return list;
    }
}
